﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Reflection;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml;

namespace MetroToolbox.Interactivity
{
    public abstract class EventTriggerBase<T> : EventTriggerBase where T : class
    {
        public new T Source { get { return base.Source as T; } }

        protected override sealed void OnSourceChanged(object oldSource, object newSource)
        {
            base.OnSourceChanged(oldSource, newSource);
            this.OnSourceChanged(oldSource as T, newSource as T);
        }

        protected virtual void OnSourceChanged(T oldSource, T newSource) { }
    }

    public abstract class EventTriggerBase : TriggerBase
    {
        private IDisposable _subscription;

        public readonly static DependencyProperty SourceNameProperty = DependencyProperty.Register(
            "SourceName", 
            typeof(string), 
            typeof(EventTriggerBase),
            new PropertyMetadata(null, SourceName_PropertyChanged));

        public string SourceName
        {
            get { return (string)this.GetValue(SourceNameProperty); }
            set { this.SetValue(SourceNameProperty, value); }
        }

        private static void SourceName_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var eventTrigger = d as EventTriggerBase;
            if (eventTrigger != null && eventTrigger.SourceObject == null)
            {
                if (e.NewValue == null)
                {
                    eventTrigger.OnSourceChanged(eventTrigger.ResolveSourceName(e.OldValue as string), eventTrigger.AssociatedObject);
                    return;
                }

                if (e.OldValue == null)
                {
                    eventTrigger.OnSourceChanged(eventTrigger.AssociatedObject, eventTrigger.ResolveSourceName(e.NewValue as string));
                    return;
                }

                eventTrigger.OnSourceChanged(eventTrigger.ResolveSourceName(e.OldValue as string), eventTrigger.ResolveSourceName(e.NewValue as string));
            }
        }

        public readonly static DependencyProperty SourceObjectProperty = DependencyProperty.Register(
            "SourceObject", 
            typeof(object), 
            typeof(EventTriggerBase), 
            new PropertyMetadata(null, SourceObject_PropertyChanged));

        public object SourceObject
        {
            get { return this.GetValue(SourceObjectProperty); }
            set { this.SetValue(SourceObjectProperty, value); }
        }

        private static void SourceObject_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var eventTrigger = d as EventTriggerBase;
            if (eventTrigger != null)
            {
                object newSource = eventTrigger.ResolveSourceName(eventTrigger.SourceName) ?? eventTrigger.AssociatedObject;
                if (e.NewValue == null)
                {
                    eventTrigger.OnSourceChanged(e.OldValue, newSource);
                    return;
                }

                if (e.OldValue == null && newSource != null)
                {
                    eventTrigger.OnSourceChanged(newSource, e.NewValue);
                    return;
                }

                eventTrigger.OnSourceChanged(e.OldValue, e.NewValue);
            }
        }

        protected virtual void OnSourceChanged(object oldSource, object newSource)
        {
            string eventName = GetEventName();
            if (String.IsNullOrWhiteSpace(eventName))
            {
                throw new InvalidOperationException("The EventName property must be set on an EventTrigger.");
            }

            if (oldSource != null && SourceTypeConstraint.GetTypeInfo().IsAssignableFrom(oldSource.GetType().GetTypeInfo()))
            {
                UnregisterEvent();
            }

            if (newSource != null && SourceTypeConstraint.GetTypeInfo().IsAssignableFrom(newSource.GetType().GetTypeInfo()))
            {
                RegisterEvent(newSource, eventName);
            }
        }

        public object Source
        {
            get
            {
                object source = null;
                if (SourceObject != null)
                {
                    source = SourceObject;

                }
                else if (!String.IsNullOrWhiteSpace(SourceName))
                {
                    // TODO: This probably isn't correct
                    source = ResolveSourceName(SourceName);
                }

                return source ?? AssociatedObject;
            }
        }

        protected Type SourceTypeConstraint { get { return typeof(object); } }

        protected override sealed Type AssociatedObjectTypeConstraint
        {
            get
            {
                var typeConstraintAttribute = this.GetType().GetTypeInfo().GetCustomAttribute<TypeConstraintAttribute>();

                if (typeConstraintAttribute != null)
                {
                    return typeConstraintAttribute.Constraint;
                }

                return base.AssociatedObjectTypeConstraint;
            }
        }

        protected virtual void OnEvent(EventArgs eventArgs)
        {
            this.InvokeActions(eventArgs);
        }

        protected abstract string GetEventName();

        protected override void OnAttached()
        {
            base.OnAttached();

            OnSourceChanged(null, Source);
        }

        protected override void OnDetaching()
        {
            OnSourceChanged(Source, null);

            base.OnDetaching();
        }

        private void RegisterEvent(object source, string eventName)
        {
            var eventArgs = from e in Observable.FromEventPattern(source, eventName) select e.EventArgs;

            _subscription = eventArgs.Subscribe(new EventTriggerObserver(this));
        }

        private void UnregisterEvent()
        {
            if (_subscription != null)
            {
                _subscription.Dispose();

                _subscription = null;
            }
        }

        private object ResolveSourceName(string sourceName)
        {
            // TODO: This probably isn't correct
            return ((FrameworkElement)AssociatedObject).FindName(sourceName);
        }

        private class EventTriggerObserver : IObserver<object>
        {
            private EventTriggerBase _trigger;

            public EventTriggerObserver(EventTriggerBase trigger)
            {
                _trigger = trigger;
            }

            public void OnCompleted()
            {
                throw new NotImplementedException();
            }

            public void OnError(Exception error)
            {
                throw new NotImplementedException();
            }

            public void OnNext(object value)
            {
                // Can this happen on a different thread?
                _trigger.InvokeActions(value);
            }
        }
    }
}
